// Copyright (c) M5Stack. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#include "RTC8563_Class.hpp"

#include <stdlib.h>

namespace m5
{
    tm rtc_datetime_t::get_tm(void) const
    {
        tm t_st = {
            time.seconds,
            time.minutes,
            time.hours,
            date.date,
            date.month - 1,
            date.year - 1900,
            date.weekDay,
            0,
            0,
        };
        return t_st;
    }

    void rtc_datetime_t::set_tm(tm& datetime)
    {
        date = rtc_date_t{datetime};
        time = rtc_time_t{datetime};
    }

    static std::uint8_t bcd2ToByte(std::uint8_t value) { return ((value >> 4) * 10) + (value & 0x0F); }

    static std::uint8_t byteToBcd2(std::uint8_t value)
    {
        std::uint_fast8_t bcdhigh = value / 10;
        return (bcdhigh << 4) | (value - (bcdhigh * 10));
    }

    bool RTC8563_Class::begin(I2C_Class* i2c)
    {
        if (i2c)
        {
            _i2c = i2c;
            i2c->begin();
        }
        /// TimerCameraの内蔵RTCが初期化に失敗することがあったため、最初に空打ちする;
        writeRegister8(0x00, 0x00);
        _init = writeRegister8(0x00, 0x00) && writeRegister8(0x0E, 0x03);
        return _init;
    }

    bool RTC8563_Class::getVoltLow(void)
    {
        return readRegister8(0x02) & 0x80; // RTCC_VLSEC_MASK
    }

    bool RTC8563_Class::getDateTime(rtc_datetime_t* datetime) const
    {
        std::uint8_t buf[7] = {0};

        if (!isEnabled() || !readRegister(0x02, buf, 7))
        {
            return false;
        }

        datetime->time.seconds = bcd2ToByte(buf[0] & 0x7f);
        datetime->time.minutes = bcd2ToByte(buf[1] & 0x7f);
        datetime->time.hours = bcd2ToByte(buf[2] & 0x3f);

        datetime->date.date = bcd2ToByte(buf[3] & 0x3f);
        datetime->date.weekDay = bcd2ToByte(buf[4] & 0x07);
        datetime->date.month = bcd2ToByte(buf[5] & 0x1f);
        datetime->date.year = bcd2ToByte(buf[6] & 0xff) + ((0x80 & buf[5]) ? 1900 : 2000);
        return true;
    }

    bool RTC8563_Class::getTime(rtc_time_t* time) const
    {
        std::uint8_t buf[3] = {0};

        if (!isEnabled() || !readRegister(0x02, buf, 3))
        {
            return false;
        }

        time->seconds = bcd2ToByte(buf[0] & 0x7f);
        time->minutes = bcd2ToByte(buf[1] & 0x7f);
        time->hours = bcd2ToByte(buf[2] & 0x3f);
        return true;
    }

    void RTC8563_Class::setTime(const rtc_time_t& time)
    {
        std::uint8_t buf[] = {byteToBcd2(time.seconds), byteToBcd2(time.minutes), byteToBcd2(time.hours)};
        writeRegister(0x02, buf, sizeof(buf));
    }

    bool RTC8563_Class::getDate(rtc_date_t* date) const
    {
        std::uint8_t buf[4] = {0};

        if (!readRegister(0x05, buf, 4))
        {
            return false;
        }

        date->date = bcd2ToByte(buf[0] & 0x3f);
        date->weekDay = bcd2ToByte(buf[1] & 0x07);
        date->month = bcd2ToByte(buf[2] & 0x1f);
        date->year = bcd2ToByte(buf[3] & 0xff) + ((0x80 & buf[2]) ? 1900 : 2000);
        return true;
    }

    void RTC8563_Class::setDate(const rtc_date_t& date)
    {
        std::uint8_t w = date.weekDay;
        if (w > 6 && date.year >= 1900 && ((std::size_t)(date.month - 1)) < 12)
        { /// weekDay auto adjust
            int32_t year = date.year;
            int32_t month = date.month;
            int32_t day = date.date;
            if (month < 3)
            {
                year--;
                month += 12;
            }
            int32_t ydiv100 = year / 100;
            w = (year + (year >> 2) - ydiv100 + (ydiv100 >> 2) + (13 * month + 8) / 5 + day) % 7;
        }

        std::uint8_t buf[] = {byteToBcd2(date.date),
                              w,
                              (std::uint8_t)(byteToBcd2(date.month) + (date.year < 2000 ? 0x80 : 0)),
                              byteToBcd2(date.year % 100)};
        writeRegister(0x05, buf, sizeof(buf));
    }

    int RTC8563_Class::setAlarmIRQ(int afterSeconds)
    {
        std::uint8_t reg_value = readRegister8(0x01) & ~0x0C;

        if (afterSeconds < 0)
        { // disable timer
            writeRegister8(0x01, reg_value & ~0x01);
            writeRegister8(0x0E, 0x03);
            return -1;
        }

        std::size_t div = 1;
        std::uint8_t type_value = 0x82;
        if (afterSeconds < 270)
        {
            if (afterSeconds > 255)
            {
                afterSeconds = 255;
            }
        }
        else
        {
            div = 60;
            afterSeconds = (afterSeconds + 30) / div;
            if (afterSeconds > 255)
            {
                afterSeconds = 255;
            }
            type_value = 0x83;
        }

        writeRegister8(0x0E, type_value);
        writeRegister8(0x0F, afterSeconds);

        writeRegister8(0x01, (reg_value | 0x01) & ~0x80);
        return afterSeconds * div;
    }

    int RTC8563_Class::setAlarmIRQ(const rtc_time_t& time)
    {
        union
        {
            std::uint32_t raw = ~0;
            std::uint8_t buf[4];
        };
        bool irq_enable = false;

        if (time.minutes >= 0)
        {
            irq_enable = true;
            buf[0] = byteToBcd2(time.minutes) & 0x7f;
        }

        if (time.hours >= 0)
        {
            irq_enable = true;
            buf[1] = byteToBcd2(time.hours) & 0x3f;
        }

        writeRegister(0x09, buf, 4);

        if (irq_enable)
        {
            bitOn(0x01, 0x02);
        }
        else
        {
            bitOff(0x01, 0x02);
        }

        return irq_enable;
    }

    int RTC8563_Class::setAlarmIRQ(const rtc_date_t& date, const rtc_time_t& time)
    {
        union
        {
            std::uint32_t raw = ~0;
            std::uint8_t buf[4];
        };
        bool irq_enable = false;

        if (time.minutes >= 0)
        {
            irq_enable = true;
            buf[0] = byteToBcd2(time.minutes) & 0x7f;
        }

        if (time.hours >= 0)
        {
            irq_enable = true;
            buf[1] = byteToBcd2(time.hours) & 0x3f;
        }

        if (date.date >= 0)
        {
            irq_enable = true;
            buf[2] = byteToBcd2(date.date) & 0x3f;
        }

        if (date.weekDay >= 0)
        {
            irq_enable = true;
            buf[3] = byteToBcd2(date.weekDay) & 0x07;
        }

        writeRegister(0x09, buf, 4);

        if (irq_enable)
        {
            bitOn(0x01, 0x02);
        }
        else
        {
            bitOff(0x01, 0x02);
        }

        return irq_enable;
    }

    bool RTC8563_Class::getIRQstatus(void) { return _init && (0x0C & readRegister8(0x01)); }

    void RTC8563_Class::clearIRQ(void)
    {
        if (!_init)
        {
            return;
        }
        bitOff(0x01, 0x0C);
    }

    void RTC8563_Class::disableIRQ(void)
    {
        if (!_init)
        {
            return;
        }
        // disable alerm (bit7:1=disabled)
        static constexpr const std::uint8_t buf[4] = {0x80, 0x80, 0x80, 0x80};
        writeRegister(0x09, buf, 4);

        // disable timer (bit7:0=disabled)
        writeRegister8(0x0E, 0);

        // clear flag and INT enable bits
        writeRegister8(0x01, 0x00);
    }

    void RTC8563_Class::setSystemTimeFromRtc(struct timezone* tz)
    {
#if !defined(M5UNIFIED_PC_BUILD)
        rtc_datetime_t dt;
        if (getDateTime(&dt))
        {
            tm t_st;
            t_st.tm_isdst = -1;
            t_st.tm_year = dt.date.year - 1900;
            t_st.tm_mon = dt.date.month - 1;
            t_st.tm_mday = dt.date.date;
            t_st.tm_hour = dt.time.hours;
            t_st.tm_min = dt.time.minutes;
            t_st.tm_sec = dt.time.seconds;
            timeval now;
            // mktime(3) uses localtime, force UTC
            char* oldtz = getenv("TZ");
            setenv("TZ", "GMT0", 1);
            tzset(); // Workaround for https://github.com/espressif/esp-idf/issues/11455
            now.tv_sec = mktime(&t_st);
            if (oldtz)
            {
                setenv("TZ", oldtz, 1);
            }
            else
            {
                unsetenv("TZ");
            }
            now.tv_usec = 0;
            settimeofday(&now, tz);
        }
#endif
    }
} // namespace m5